#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         RandomAssessmentCreatorAccessory.py
#
# Copyright:    Copyright (c) 2005, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  This file defines the RandomAssessmentCreatorAccessory class,
#               that provides a mechanism to create a number of 
#               random assessments for a Model
#
# See Also:     Accessory, Model, Assessment, AssessmentStorageService
#####################################################################

#####################################################################
## KnowledgeEngine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Accessory import *
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.WizardStepDefinition import *

#####################################################################
## Zope Library Imports
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager
 
#####################################################################
## Python Library Imports
from random import randint, choice
from sets import Set
import sys;

#####################################################################
## RandomAssessmentCreatorAccessory Class Definition
class RandomAssessmentCreatorAccessory(Accessory):
    """
        An interface specifying the standard features of an Accessory
    """

    #---------------------------------------------------------------#

    #define security
    security = ClassSecurityInfo()
    security.declareObjectPublic()
    security.setDefaultAccess("allow")
        
    #---------------------------------------------------------------#

    def getIdentity(self):
        """
            ABSTRACT: Return the unique identity of the accessory
            in the Knowledge Engine
        """
        
        #typically this will be the same as the class name
        return "RandomAssessmentCreatorAccessory"
        
    #---------------------------------------------------------------#
        
    def getTitle(self, genericObject):
        """
            Returns the title of the Accessory (when
            applied to the specified genericObject)
            
            RETURN TYPE: String
        """

        return "Random Assessment Creator"
        
    #---------------------------------------------------------------#
        
    def getPurpose(self, genericObject):
        """
            ABSTRACT: Returns the purpose of the Accessory (when
            applied to the specified genericObject)
            
            RETURN TYPE: String
        """

        return "Creates a number of randomly generated assessments for this Model.  Useful for testing."
        
    #---------------------------------------------------------------#
        
    def getIconName(self, genericObject):
        """
            ABSTRACT: Returns the name of the Icon of the Accessory (when
            applied to the specified genericObject).
            
            The IconName represents the image file name that will be found using
            acquisition.
            
            RETURN TYPE: String
        """
        
        return "random.gif"
        
    #---------------------------------------------------------------#
        
    def getExecuteActionDisplayName(self, genericObject):
        """
            ABSTRACT: Returns the display name for the execute action of the Accessory (when
            applied to the specified genericObject)
            
            RETURN TYPE: String
        """

        #return different action names based on the locked status of the model
        return "Generate"
        
    #---------------------------------------------------------------#
        
    def isAvailableForGenericObject(self, genericObject):
        """
            Returns if the accessory is available for the 
            specified genericObject.
            
            This method is called to determine if an Accessory
            should be displayed for the specified genericObject 
            when rendering it's property editor etc.

            RETURN TYPE: Boolean
        """

        return genericObject.isInstanceOf("Model")
        
    #---------------------------------------------------------------#
        
    def getWizardStepDefinitions(self, context=None):
        """
            ABSTRACT: This method is used by this class to
            determine the actual wizard step definitions
            for the wizard
            
            RETURN: [WizardStepDefinition]
        """

        wizardStep1Definition = WizardStepDefinition("Generation Options", "Please choose the Assessment Generation Options")
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="nrAssessments", displayName="How Many Assessments?", description="The number of assessments containing randomly generated content to create.", defaultValue=0, propertyType=pt_INTEGER, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC, isForConstructor=false, hasEnumeratedValues=false))        
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="repositoryIdentities", displayName="Repositories...", description="The repositories in which the randomly created assessments will be placed.", defaultValue=[], propertyType=pt_STRING, propertyFormat=pf_NATIVE, propertyStructure=ps_LIST, hasEnumeratedValues=true, enumeratedValuesMapping=[('(Anonymous User)', 'anonymous'), ('(Chosen User Repository)', 'user')] + [(repository.getTitleOrId(),repository.getIdentity()) for repository in context.getRepositories()]))
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="dateFrom", displayName="Create From...", description="The date from which assessments will be randomly created (inclusive)", defaultValue=None, propertyType=pt_DATE, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC, isForConstructor=false, hasEnumeratedValues=false))        
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="dateTo", displayName="Create To...", description="The date to which assessments will be randomly created (inclusive)", defaultValue=None, propertyType=pt_DATE, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC, isForConstructor=false, hasEnumeratedValues=false))        
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="userIdentities", displayName="Users...", description="The users for which the randomly created assessments will be created.", defaultValue=[], propertyType=pt_STRING, propertyFormat=pf_NATIVE, propertyStructure=ps_LIST, hasEnumeratedValues=true, enumeratedValuesMapping=[('(Anonymous User)', 'anonymous')] + [(user.getFullname(),user.getIdentity()) for user in context.getUserProviderService().getUsers()]))

        return [wizardStep1Definition]
        
    #---------------------------------------------------------------#
        
    def generateRandomAssessmentForComponent(self, component, assessment):
        """
            Generates random content for the specified component in
            the provided assessment
            (does not save the assessment)
        """
        
        # TODO - Move logic out to each component, call component.randomize() or somesuch. - jamesd
        
        print "Generating randomness for %s ( %s )" % ( component.getIdentity(), component.getClassName() )
        
        if hasattr(component, 'randomize'):
            component.randomize( assessment )
            return
        
        #generate component content based on the type of the component
        if component.isInstanceOf("CheckBox"):
            
            component.updateAssessmentState( assessment, randint(0,1) == 1 )
    
        elif component.isInstanceOf("TextBox"):

            # 20070417 johnm    Seems the text randomiser sometimes returns
            #                   an empty string. Set to keep going until we
            #                   get something back.
            string = None
            while not string:
                if component.getIsMultiline():
                    string = Markov.getWords(randint(50,100))
                else:
                    string = Markov.getWords(randint(5,10))
                            
            component.updateAssessmentState( assessment, string )


        elif component.isInstanceOf("NumericBox"):
            
            
            min = 0
            max = 9999
            
            restriction = component.getRangeRestriction()
            if restriction: # TODO - Be more specific about the restriction
                min = component.getMinimumValue()
                max = component.getMaximumValue()
                
            value = randint(min, max)
            component.updateAssessmentState( assessment, value )
            

        elif component.isInstanceOf("Link"):
                        
            vocab = list(component.getAnswerList())
            if vocab:
                value = choice(vocab) # use random.choice to pick a value from the vocab            
                component.updateAssessmentState( assessment, value[0]  )
            
        elif component.isInstanceOf("Lookup"):
                        
            vocab = list(component.getAnswerList(assessment))
            if vocab:
                value = choice(vocab)[0] # use random.choice to pick a value from the vocab
                component.updateAssessmentState( assessment, value  )            
            
        elif component.isInstanceOf("PythonScript"):
                        
            value = component.executeScript( assessment )
            component.updateAssessmentState( assessment, value )                        
            
        elif component.isInstanceOf("DateTime"):
            
            start = DateTime("2000-01-01").timeTime()
            end   = DateTime("2010-12-31").timeTime()
            
            when = DateTime( randint( start, end ) )
            
            component.updateAssessmentState( assessment, when )

        elif component.isInstanceOf("Selection") and component.containsChildren():
            #how many selections to make (single or multiple selection)
            if component.isMultipleSelectionPermitted():
                nrSelections = randint(1, len(component.getChildren()))              
            else:
                nrSelections = 1
            
            #create a set of selected components
            optionIdentitiesToSelect = Set()
            while nrSelections > 0:
                #randomly chose one of the options
                optionIdentitiesToSelect.add(component.getChildren()[randint(0, len(component.getChildren())-1)].getIdentity())
                
                #one less option to choose
                nrSelections = nrSelections -1
                
            #select/unselect the options
            for option in component.getChildren():
                
                option.updateAssessmentState( assessment, option.getIdentity() in optionIdentitiesToSelect)
                #create default state for the child
                #state = option.getDefaultAssessmentState(assessment)
                #state.setValue(option.getIdentity() in optionIdentitiesToSelect)

                #add to the assessment
                #assessment.setComponentState(option.getIdentity(), state)
            
        elif component.isInstanceOf("MultipleChoice"):
            #create default state
            #state = component.getDefaultAssessmentState(assessment)
        
            #get the possible answers
            answers = component.getAnswerList()
        
            #make it random
            #state.setValue(answers[randint(0, len(answers)-1)].getAnswerID())

            #add to the assessment
            #assessment.setComponentState(component.getIdentity(), state)
            
            component.updateAssessmentState( assessment, answers[randint(0, len(answers)-1)].getAnswerID())

        elif component.isContainer():
            #add random assessment content for the components in the model
            for child in component.getChildren():            
                self.generateRandomAssessmentForComponent(child, assessment)




    #---------------------------------------------------------------#

    def getWizardStepFinishButtonTitle(self, wizardState, context=None):
        """
            Return the title of the finished button for the
            wizard step using the provided wizard state.
            
            PARAMS:
            wizardState - the current properties of the wizard in action
            context - usually a GenericObject instance that may 
                      be used to dynamically determine wizard
                      step definitions
            
            By default returns 'Finish...'.  Override this
            method if you'd like something else.

            RETURN: String
        """            
        
        return "Generate..."
        
    #---------------------------------------------------------------#
        
    def executeAccessoryWith(self, genericObject, wizardState):
        """
            Executes the accessory for the specified
            genericObject, using the specified wizardState.            
            
            Returns a pair indicating the success of the accessory on
            the component and a corresponding message.
            
            RETURN TYPE: (Boolean, String)
        """
 
        #get the model
        model = genericObject

        #get the assessment storage service
        assessmentStorageService = model.getAssessmentStorageService()        
        
        #determine the total number of assessments
        totalNrAssessments = assessmentStorageService.getNrAssessmentsForModel(model)
        
        #get the number of assessments to generate
        nrAssessments = wizardState.getPropertyValue("nrAssessments")
        
        #get the users that may own the assessments (set to 'anonymous' if none have been selected)
        userIdentities = wizardState.getPropertyValue("userIdentities")
        
        #the starting date and ending dates
        dateFrom = wizardState.getPropertyValue("dateFrom")
        dateTo = wizardState.getPropertyValue("dateTo")
        if dateTo.lessThan(dateFrom):
            tmp = dateFrom
            dateFrom = dateTo
            dateTo = dateFrom

        #determine the difference in days between the specified dates
        secondsDifference = int(dateTo.timeTime() - dateFrom.timeTime())
        minutesDifference = int(secondsDifference / 60)
        hoursDifference = int(minutesDifference / 60)
        daysDifference = int(hoursDifference / 24)
        
        #generate the assessments
        assessmentNr = 1
        while assessmentNr <= nrAssessments:
            #determine a random creation time
            creationTime = DateTime(dateFrom.timeTime() + randint(0, (daysDifference + 1) * 24 * 60 * 60)-1)   
        
            #choose a user that created the assessment
            if len(userIdentities) == 0:
                userIdentity = 'anonymous'
            else:         
                userIdentity = userIdentities[randint(0,len(userIdentities)-1)]
        
            #create a assessment
            assessment = assessmentStorageService.createAssessment(model)
            assessment.setName("Assessment %d" % (assessmentNr + totalNrAssessments))
            assessment.setCreatedBy(userIdentity)
            assessment.setInstantCreated(creationTime)
            assessment.setInstantModified(creationTime)
            
            #add random assessment content for the components in the model
            for component in model.getSections():            
                self.generateRandomAssessmentForComponent(component, assessment)
            
            #save the assessment
            assessmentStorageService.saveAssessment(assessment)

            #add the assessment to the appropriate repositories
            for repositoryIdentity in wizardState.getPropertyValue("repositoryIdentities"):
                #place in the 'user' repository?
                if repositoryIdentity == 'user':
                    assessmentStorageService.addAssessmentToRepository(assessment, userIdentity)
                else:
                    assessmentStorageService.addAssessmentToRepository(assessment, repositoryIdentity)
        
            #next assessment
            assessmentNr = assessmentNr + 1
 
        #always successful
        return (true, "Successfully Generated %d Assessments" % nrAssessments)
    
    #---------------------------------------------------------------#

#####################################################################
## Class Initialisation

# Register Accessory Information with the Knowledge Engine
registerAccessory(RandomAssessmentCreatorAccessory())

#####################################################################

#the following text is used as a random number generator seed.
markov_text = """
As is well known, the fundamental law of the mechanics of
Galilei-Newton, which is known as the law of inertia, can be stated
thus: A body removed sufficiently far from other bodies continues in a
state of rest or of uniform motion in a straight line. This law not
only says something about the motion of the bodies, but it also
indicates the reference-bodies or systems of coordinates, permissible
in mechanics, which can be used in mechanical description. The visible
fixed stars are bodies for which the law of inertia certainly holds to
a high degree of approximation. Now if we use a system of co-ordinates
which is rigidly attached to the earth, then, relative to this system,
every fixed star describes a circle of immense radius in the course of
an astronomical day, a result which is opposed to the statement of the
law of inertia. So that if we adhere to this law we must refer these
motions only to systems of coordinates relative to which the fixed
stars do not move in a circle. A system of co-ordinates of which the
state of motion is such that the law of inertia holds relative to it
is called a " Galileian system of co-ordinates." The laws of the
mechanics of Galflei-Newton can be regarded as valid only for a
Galileian system of co-ordinates.



THE PRINCIPLE OF RELATIVITY
(IN THE RESTRICTED SENSE)


In order to attain the greatest possible clearness, let us return to
our example of the railway carriage supposed to be travelling
uniformly. We call its motion a uniform translation ("uniform" because
it is of constant velocity and direction, " translation " because
although the carriage changes its position relative to the embankment
yet it does not rotate in so doing). Let us imagine a raven flying
through the air in such a manner that its motion, as observed from the
embankment, is uniform and in a straight line. If we were to observe
the flying raven from the moving railway carriage. we should find that
the motion of the raven would be one of different velocity and
direction, but that it would still be uniform and in a straight line.
Expressed in an abstract manner we may say : If a mass m is moving
uniformly in a straight line with respect to a co-ordinate system K,
then it will also be moving uniformly and in a straight line relative
to a second co-ordinate system K1 provided that the latter is
executing a uniform translatory motion with respect to K. In
accordance with the discussion contained in the preceding section, it
follows that:

If K is a Galileian co-ordinate system. then every other co-ordinate
system K' is a Galileian one, when, in relation to K, it is in a
condition of uniform motion of translation. Relative to K1 the
mechanical laws of Galilei-Newton hold good exactly as they do with
respect to K.

We advance a step farther in our generalisation when we express the
tenet thus: If, relative to K, K1 is a uniformly moving co-ordinate
system devoid of rotation, then natural phenomena run their course
with respect to K1 according to exactly the same general laws as with
respect to K. This statement is called the principle of relativity (in
the restricted sense).

As long as one was convinced that all natural phenomena were capable
of representation with the help of classical mechanics, there was no
need to doubt the validity of this principle of relativity. But in
view of the more recent development of electrodynamics and optics it
became more and more evident that classical mechanics affords an
insufficient foundation for the physical description of all natural
phenomena. At this juncture the question of the validity of the
principle of relativity became ripe for discussion, and it did not
appear impossible that the answer to this question might be in the
negative.

Nevertheless, there are two general facts which at the outset speak
very much in favour of the validity of the principle of relativity.
Even though classical mechanics does not supply us with a sufficiently
broad basis for the theoretical presentation of all physical
phenomena, still we must grant it a considerable measure of " truth,"
since it supplies us with the actual motions of the heavenly bodies
with a delicacy of detail little short of wonderful. The principle of
relativity must therefore apply with great accuracy in the domain of
mechanics. But that a principle of such broad generality should hold
with such exactness in one domain of phenomena, and yet should be
invalid for another, is a priori not very probable.

We now proceed to the second argument, to which, moreover, we shall
return later. If the principle of relativity (in the restricted sense)
does not hold, then the Galileian co-ordinate systems K, K1, K2, etc.,
which are moving uniformly relative to each other, will not be
equivalent for the description of natural phenomena. In this case we
should be constrained to believe that natural laws are capable of
being formulated in a particularly simple manner, and of course only
on condition that, from amongst all possible Galileian co-ordinate
systems, we should have chosen one (K[0]) of a particular state of
motion as our body of reference. We should then be justified (because
of its merits for the description of natural phenomena) in calling
this system " absolutely at rest," and all other Galileian systems K "
in motion." If, for instance, our embankment were the system K[0] then
our railway carriage would be a system K, relative to which less
simple laws would hold than with respect to K[0]. This diminished
simplicity would be due to the fact that the carriage K would be in
motion (i.e."really")with respect to K[0]. In the general laws of
nature which have been formulated with reference to K, the magnitude
and direction of the velocity of the carriage would necessarily play a
part. We should expect, for instance, that the note emitted by an
organpipe placed with its axis parallel to the direction of travel
would be different from that emitted if the axis of the pipe were
placed perpendicular to this direction.

Now in virtue of its motion in an orbit round the sun, our earth is
comparable with a railway carriage travelling with a velocity of about
30 kilometres per second. If the principle of relativity were not
valid we should therefore expect that the direction of motion of the
earth at any moment would enter into the laws of nature, and also that
physical systems in their behaviour would be dependent on the
orientation in space with respect to the earth. For owing to the
alteration in direction of the velocity of revolution of the earth in
the course of a year, the earth cannot be at rest relative to the
hypothetical system K[0] throughout the whole year. However, the most
careful observations have never revealed such anisotropic properties
in terrestrial physical space, i.e. a physical non-equivalence of
different directions. This is very powerful argument in favour of the
principle of relativity.
"""

class Markov:
    """ Random text generator, uses markov as the seed """
    
    # Markov algorithm used from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/194364
    seed = markov_text
    table = {}
    nonword = "\n" # Since we split on whitespace, this can never be a word
    w1 = nonword
    w2 = nonword
    # Build markov tables on init
    for line in seed:
        for word in line.split():
            table.setdefault( (w1, w2), [] ).append(word)
            w1, w2 = w2, word
            
    table.setdefault( (w1, w2), [] ).append(nonword) # Mark the end of the file
            

    def getWords(cls, maxwords=10):
        """
            Return a random number of words
        """
        
        SKIP = 50 # Skip the first n words (They tend to be less random)
        w1 = cls.nonword
        w2 = cls.nonword
        output = []
        for i in xrange(maxwords + SKIP):
            newword = choice(cls.table[(w1, w2)])
            if newword == cls.nonword: 
                return ' '.join(output)
            
            if i > SKIP: output.append( newword )
            w1, w2 = w2, newword
            
        return " ".join(output)
            
    getWords = classmethod(getWords) # Use old decorator syntax for Python 2.3 compat            
